/**
* Copyright (c) 2007 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM - Initial API and implementation
*/
package org.eclipse.emf.ecore.resource.impl;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Key;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.eclipse.emf.ecore.resource.URIConverter;
/**
* <p>EMF implementation for the {@link URIConverter.Cipher} interface using
* the AES encryption algorithm.</p>
* <p>This shows how this class can be used:</p>
* <pre>
* Map options = new HashMap();
* options.put(Resource.OPTION_CIPHER,
* new AESCipherImpl("12345")); // "That's amazing! I've got the same combination on my luggage!"
* resource.save(options);
* resource.load(options);
* </pre>
*/
public class AESCipherImpl implements URIConverter.Cipher
{
private static final String ENCRYPTION_ALGORITHM = "AES/CFB8/PKCS5Padding";
private static final int ENCRYPTION_IV_LENGTH = 16;
private static final String ENCRYPTION_KEY_ALGORITHM = "AES";
private static final String PBE_ALGORITHM = "PBEWithMD5AndDES";
private static final int PBE_IV_LENGTH = 8;
private static final int PBE_ITERATIONS = 1000;
private static KeyGenerator keygen;
private static SecureRandom random;
private static Key generateKey(int keysize)
{
if (keygen == null)
{
try
{
keygen = KeyGenerator.getInstance(ENCRYPTION_KEY_ALGORITHM);
keygen.init(keysize);
}
catch (Exception ex)
{
// all implementations of Java 1.5 should support AES
throw new RuntimeException(ex);
}
}
return keygen.generateKey();
}
private static byte[] randomBytes(int length)
{
if (random == null)
{
random = new SecureRandom();
}
byte[] bytes = new byte [length];
random.nextBytes(bytes);
return bytes;
}
private static byte[] readBytes(int length, InputStream in) throws Exception
{
byte[] bytes = new byte [length];
int read = in.read(bytes);
if (read != length)
{
throw new Exception("expected length != actual length");
}
return bytes;
}
private static byte[] transformWithPassword(byte[] bytes, byte[] iv, String password, int mode) throws Exception
{
// generate the key
PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(PBE_ALGORITHM);
SecretKey pbeKey = keyFactory.generateSecret(pbeKeySpec);
PBEParameterSpec pbeParamSpec = new PBEParameterSpec(iv, PBE_ITERATIONS);
// encrypt the input
Cipher keyCipher = Cipher.getInstance(PBE_ALGORITHM);
keyCipher.init(mode, pbeKey, pbeParamSpec);
return keyCipher.doFinal(bytes);
}
private String password;
private Key key;
private int keysize = 128;
private byte[] encryptedKeyBytes;
private byte[] pbeIV;
private byte[] encryptionIV;
public AESCipherImpl(String password) throws Exception
{
this.password = password;
}
/**
* <p>Sets the key size to be used when creating the AES key. Using anything
* larger than 128 may make the data file non-portable.</p>
* <p>The key size cannot be changed after this Cipher is used.</p>
*/
public void setKeysize(int keysize)
{
if (key == null)
{
this.keysize = keysize;
}
}
public int getKeysize()
{
return keysize;
}
public OutputStream encrypt(OutputStream outputStream) throws Exception
{
// If we haven't yet encrypted or decrypted, generate a key. This key will
// only be used for encryption. Decryption keys are always derived from
// the header of the input stream itself.
if (key == null)
{
// this is the key we will use to encrypt the data
key = generateKey(getKeysize());
// create the IV for the password generation algorithm
pbeIV = randomBytes(PBE_IV_LENGTH);
// generate the IV for encryption
encryptionIV = randomBytes(ENCRYPTION_IV_LENGTH);
// turn the password into an AES key
encryptedKeyBytes = transformWithPassword(key.getEncoded(), pbeIV, password, Cipher.ENCRYPT_MODE);
}
// write the header to the output stream. this has the format
// (delimeters are not written):
// PBE IV|ENCRYPTION IV|ENCRYPTED KEY LENGTH|ENCRYPTED KEY
outputStream.write(pbeIV);
outputStream.write(encryptionIV);
outputStream.write(encryptedKeyBytes.length);
outputStream.write(encryptedKeyBytes);
// now create the encryption cipher
Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(encryptionIV));
// The CipherOutputStream shoudln't close the underlying stream
//
outputStream = new FilterOutputStream(outputStream)
{
@Override
public void close() throws IOException
{
// Do nothing
}
};
return new CipherOutputStream(outputStream, cipher);
}
public void finish(OutputStream out) throws Exception
{
out.close();
}
public InputStream decrypt(InputStream in) throws Exception
{
// Read the header of the encrypted file.
byte[] pbeIV = readBytes(PBE_IV_LENGTH, in);
byte[] encryptionIV = readBytes(ENCRYPTION_IV_LENGTH, in);
int keyLength = in.read();
byte[] encryptedKeyBytes = readBytes(keyLength, in);
// Decrypt the key bytes
byte[] decryptedKeyBytes = transformWithPassword(encryptedKeyBytes, pbeIV, password, Cipher.DECRYPT_MODE);
// Create the key from the key bytes
Key key = new SecretKeySpec(decryptedKeyBytes, ENCRYPTION_KEY_ALGORITHM);
// If we haven't yet generated a key, just use this one
if (this.key == null)
{
this.pbeIV = pbeIV;
this.encryptionIV = encryptionIV;
this.encryptedKeyBytes = encryptedKeyBytes;
this.key = key;
}
// now create the decrypt cipher
Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(encryptionIV));
return new CipherInputStream(in, cipher);
}
public void finish(InputStream in) throws Exception
{
// Do nothing
}
}